/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ryan.ohos.extension.nested;

import com.ryan.ohos.extension.Gravity;
import com.ryan.ohos.extension.MathUtils;
import com.ryan.ohos.extension.TextPaint;
import com.ryan.ohos.extension.TextUtils;
import com.ryan.ohos.extension.scroller.animation.AnimationUtils;
import com.ryan.ohos.extension.scroller.animation.TimeInterpolator;
import ohos.agp.colors.RgbColor;
import ohos.agp.components.Component;
import ohos.agp.render.BlendMode;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.text.Font;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.media.image.PixelMap;

import static java.lang.Math.min;

/**
 * Helper class for rendering and animating collapsed text.
 */
public final class CollapsingTextHelper {

    // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
    // by using our own texture
    private static final boolean USE_SCALING_TEXTURE = false;
    private static final String TAG = "CollapsingTextHelper";
    private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)

    private static final boolean DEBUG_DRAW = false;
    private static final Paint DEBUG_DRAW_PAINT;

    static {
        DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
        if (DEBUG_DRAW_PAINT != null) {
            DEBUG_DRAW_PAINT.setAntiAlias(true);
            DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
        }
    }

    private final Component view;

    private boolean drawTitle;
    private float expandedFraction;

    private final Rect expandedBounds;
    private final Rect collapsedBounds;
    private final RectFloat currentBounds;
    private int expandedTextGravity = Gravity.CENTER_VERTICAL;
    private int collapsedTextGravity = Gravity.CENTER_VERTICAL;
    private float expandedTextSize = 15;
    private float collapsedTextSize = 15;
    private int expandedTextColor;
    private int collapsedTextColor;

    private float expandedDrawY;
    private float collapsedDrawY;
    private float expandedDrawX;
    private float collapsedDrawX;
    private float currentDrawX;
    private float currentDrawY;
    private Font collapsedTypeface;
    private Font expandedTypeface;
    private Font currentTypeface;
//    private CancelableFontCallback expandedFontCallback;
//    private CancelableFontCallback collapsedFontCallback;

    private String text;
    private String textToDraw;
    private boolean isRtl;

    private boolean useTexture;
    private PixelMap expandedTitleTexture;
    private Paint texturePaint;

    private float scale;
    private float currentTextSize;

    private int[] state;

    private boolean boundsChanged;

    private final TextPaint textPaint;
    private final TextPaint tmpPaint;

    private TimeInterpolator positionInterpolator;
    private TimeInterpolator textSizeInterpolator;

    private float collapsedShadowRadius;
    private float collapsedShadowDx;
    private float collapsedShadowDy;
    private int collapsedShadowColor;

    private float expandedShadowRadius;
    private float expandedShadowDx;
    private float expandedShadowDy;
    private int expandedShadowColor;
//    private StaticLayout textLayout;
    private float collapsedTextBlend;
    private float expandedTextBlend;
    private float expandedFirstLineDrawX;
    private String textToDrawCollapsed;
    private int maxLines = 1;

    public CollapsingTextHelper(Component view) {
        this.view = view;

        textPaint = new TextPaint(); // Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL_STYLE);
        tmpPaint = new TextPaint(textPaint);
        tmpPaint.setAntiAlias(true);
        tmpPaint.setStyle(Paint.Style.FILL_STYLE);

        collapsedBounds = new Rect();
        expandedBounds = new Rect();
        currentBounds = new RectFloat();
    }

    public void setTextSizeInterpolator(TimeInterpolator interpolator) {
        textSizeInterpolator = interpolator;
        recalculate();
    }

    public void setPositionInterpolator(TimeInterpolator interpolator) {
        positionInterpolator = interpolator;
        recalculate();
    }

    public void setExpandedTextSize(float textSize) {
        if (expandedTextSize != textSize) {
            expandedTextSize = textSize;
            recalculate();
        }
    }

    public void setCollapsedTextSize(float textSize) {
        if (collapsedTextSize != textSize) {
            collapsedTextSize = textSize;
            recalculate();
        }
    }

    public void setCollapsedTextColor(int textColor) {
        if (collapsedTextColor != textColor) {
            collapsedTextColor = textColor;
            recalculate();
        }
    }

    public void setExpandedTextColor(int textColor) {
        if (expandedTextColor != textColor) {
            expandedTextColor = textColor;
            recalculate();
        }
    }

    public void setExpandedBounds(int left, int top, int right, int bottom) {
        if (!rectEquals(expandedBounds, left, top, right, bottom)) {
            expandedBounds.set(left, top, right, bottom);
            boundsChanged = true;
            onBoundsChanged();
        }
    }

    public void setExpandedBounds(Rect bounds) {
        setExpandedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
    }

    public void setCollapsedBounds(int left, int top, int right, int bottom) {
        if (!rectEquals(collapsedBounds, left, top, right, bottom)) {
            collapsedBounds.set(left, top, right, bottom);
            boundsChanged = true;
            onBoundsChanged();
        }
    }

    public void setCollapsedBounds(Rect bounds) {
        setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
    }

    public void getCollapsedTextActualBounds(RectFloat bounds, int labelWidth, int textGravity) {
        isRtl = calculateIsRtl(text);
        bounds.left = getCollapsedTextLeftBound(labelWidth, textGravity);
        bounds.top = collapsedBounds.top;
        bounds.right = getCollapsedTextRightBound(bounds, labelWidth, textGravity);
        bounds.bottom = collapsedBounds.top + getCollapsedTextHeight();
    }

    private float getCollapsedTextLeftBound(int width, int gravity) {
        if (gravity == Gravity.CENTER
                || (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) {
            return width / 2f - calculateCollapsedTextWidth() / 2;
        } else if ((gravity & Gravity.END) == Gravity.END
                || (gravity & Gravity.RIGHT) == Gravity.RIGHT) {
            return isRtl ? collapsedBounds.left : (collapsedBounds.right - calculateCollapsedTextWidth());
        } else {
            return isRtl ? (collapsedBounds.right - calculateCollapsedTextWidth()) : collapsedBounds.left;
        }
    }

    private float getCollapsedTextRightBound(RectFloat bounds, int width, int gravity) {
        if (gravity == Gravity.CENTER
                || (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.CENTER_HORIZONTAL) {
            return width / 2f + calculateCollapsedTextWidth() / 2;
        } else if ((gravity & Gravity.END) == Gravity.END
                || (gravity & Gravity.RIGHT) == Gravity.RIGHT) {
            return isRtl ? (bounds.left + calculateCollapsedTextWidth()) : collapsedBounds.right;
        } else {
            return isRtl ? collapsedBounds.right : (bounds.left + calculateCollapsedTextWidth());
        }
    }

    public float calculateCollapsedTextWidth() {
        if (text == null) {
            return 0;
        }
        getTextPaintCollapsed(tmpPaint);
        return tmpPaint.measureText(text);
    }

    public float getExpandedTextHeight() {
        getTextPaintExpanded(tmpPaint);
        // Return expanded height measured from the baseline.
        return -tmpPaint.ascent();
    }

    public float getCollapsedTextHeight() {
        getTextPaintCollapsed(tmpPaint);
        // Return collapsed height measured from the baseline.
        return -tmpPaint.ascent();
    }

    private void getTextPaintExpanded(TextPaint textPaint) {
        textPaint.setTextSize((int) expandedTextSize);
        textPaint.setFont(expandedTypeface);
    }

    private void getTextPaintCollapsed(TextPaint textPaint) {
        textPaint.setTextSize((int) collapsedTextSize);
        textPaint.setFont(collapsedTypeface);
    }

    void onBoundsChanged() {
        drawTitle =
                collapsedBounds.right - collapsedBounds.left > 0
                        && collapsedBounds.bottom - collapsedBounds.top > 0
                        && expandedBounds.right - expandedBounds.left > 0
                        && expandedBounds.bottom - expandedBounds.top > 0;
    }

    public void setExpandedTextGravity(int gravity) {
        if (expandedTextGravity != gravity) {
            expandedTextGravity = gravity;
            recalculate();
        }
    }

    public int getExpandedTextGravity() {
        return expandedTextGravity;
    }

    public void setCollapsedTextGravity(int gravity) {
        if (collapsedTextGravity != gravity) {
            collapsedTextGravity = gravity;
            recalculate();
        }
    }

    public int getCollapsedTextGravity() {
        return collapsedTextGravity;
    }

    public void setCollapsedTextAppearance(int size, int color) {
//        TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);

        collapsedTextColor = color;
        if (size != 0) {
            collapsedTextSize = size;
        }
//        if (textAppearance.shadowColor != null) {
//            collapsedShadowColor = textAppearance.shadowColor;
//        }
//        collapsedShadowDx = textAppearance.shadowDx;
//        collapsedShadowDy = textAppearance.shadowDy;
//        collapsedShadowRadius = textAppearance.shadowRadius;

        // Cancel pending async fetch, if any, and replace with a new one.
//        if (collapsedFontCallback != null) {
//            collapsedFontCallback.cancel();
//        }
//        collapsedFontCallback =
//                new CancelableFontCallback(
//                        new ApplyFont() {
//                            @Override
//                            public void apply(Typeface font) {
//                                setCollapsedTypeface(font);
//                            }
//                        },
//                        textAppearance.getFallbackFont());
//        textAppearance.getFontAsync(view.getContext(), collapsedFontCallback);

        recalculate();
    }

    public void setExpandedTextAppearance(int size, int color) {
//        TextAppearance textAppearance = new TextAppearance(view.getContext(), resId);
        expandedTextColor = color;
        if (size != 0) {
            expandedTextSize = size;
        }
//        if (textAppearance.shadowColor != null) {
//            expandedShadowColor = textAppearance.shadowColor;
//        }
//        expandedShadowDx = textAppearance.shadowDx;
//        expandedShadowDy = textAppearance.shadowDy;
//        expandedShadowRadius = textAppearance.shadowRadius;
//
//        // Cancel pending async fetch, if any, and replace with a new one.
//        if (expandedFontCallback != null) {
//            expandedFontCallback.cancel();
//        }
//        expandedFontCallback =
//                new CancelableFontCallback(
//                        new ApplyFont() {
//                            @Override
//                            public void apply(Typeface font) {
//                                setExpandedTypeface(font);
//                            }
//                        },
//                        textAppearance.getFallbackFont());
//        textAppearance.getFontAsync(view.getContext(), expandedFontCallback);

        recalculate();
    }

    public void setCollapsedTypeface(Font typeface) {
        if (setCollapsedTypefaceInternal(typeface)) {
            recalculate();
        }
    }

    public void setExpandedTypeface(Font typeface) {
        if (setExpandedTypefaceInternal(typeface)) {
            recalculate();
        }
    }

    public void setTypefaces(Font typeface) {
        boolean collapsedFontChanged = setCollapsedTypefaceInternal(typeface);
        boolean expandedFontChanged = setExpandedTypefaceInternal(typeface);
        if (collapsedFontChanged || expandedFontChanged) {
            recalculate();
        }
    }

    @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
    private boolean setCollapsedTypefaceInternal(Font typeface) {
        // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
        // already updated one when async op comes back after a while.
//        if (collapsedFontCallback != null) {
//            collapsedFontCallback.cancel();
//        }
        if (collapsedTypeface != typeface) {
            collapsedTypeface = typeface;
            return true;
        }
        return false;
    }

    @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
    private boolean setExpandedTypefaceInternal(Font typeface) {
        // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding
        // already updated one when async op comes back after a while.
//        if (expandedFontCallback != null) {
//            expandedFontCallback.cancel();
//        }
        if (expandedTypeface != typeface) {
            expandedTypeface = typeface;
            return true;
        }
        return false;
    }

    public Font getCollapsedTypeface() {
        return collapsedTypeface != null ? collapsedTypeface : Font.DEFAULT;
    }

    public Font getExpandedTypeface() {
        return expandedTypeface != null ? expandedTypeface : Font.DEFAULT;
    }

    /**
     * Set the value indicating the current scroll value. This decides how much of the background will
     * be displayed, as well as the title metrics/positioning.
     *
     * <p>A value of {@code 0.0} indicates that the layout is fully expanded. A value of {@code 1.0}
     * indicates that the layout is fully collapsed.
     */
    public void setExpansionFraction(float fraction) {
        fraction = MathUtils.clamp(fraction, 0f, 1f);

        if (fraction != expandedFraction) {
            expandedFraction = fraction;
            calculateCurrentOffsets();
        }
    }

    public final boolean setState(final int[] state) {
        this.state = state;

        if (isStateful()) {
            recalculate();
            return true;
        }

        return false;
    }

    public final boolean isStateful() {
        return false; // (collapsedTextColor != null && collapsedTextColor.isStateful())
                // || (expandedTextColor != null && expandedTextColor.isStateful());
    }

    public float getExpansionFraction() {
        return expandedFraction;
    }

    public float getCollapsedTextSize() {
        return collapsedTextSize;
    }

    public float getExpandedTextSize() {
        return expandedTextSize;
    }

    private void calculateCurrentOffsets() {
        calculateOffsets(expandedFraction);
    }

    private void calculateOffsets(final float fraction) {
        interpolateBounds(fraction);
        currentDrawX = lerp(expandedDrawX, collapsedDrawX, fraction, positionInterpolator);
        currentDrawY = lerp(expandedDrawY, collapsedDrawY, fraction, positionInterpolator);

        setInterpolatedTextSize(
                lerp(expandedTextSize, collapsedTextSize, fraction, textSizeInterpolator));

        setCollapsedTextBlend(
                1 - lerp(0, 1, 1 - fraction, AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR));
        setExpandedTextBlend(lerp(1, 0, fraction, AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR));

        if (collapsedTextColor != expandedTextColor) {
            // If the collapsed and expanded text colors are different, blend them based on the
            // fraction
            textPaint.setColor(
                    new Color(blendColors(getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction)));
        } else {
            textPaint.setColor(new Color(getCurrentCollapsedTextColor()));
        }

//        textPaint.setShadowLayer(
//                lerp(expandedShadowRadius, collapsedShadowRadius, fraction, null),
//                lerp(expandedShadowDx, collapsedShadowDx, fraction, null),
//                lerp(expandedShadowDy, collapsedShadowDy, fraction, null),
//                blendColors(
//                        getCurrentColor(expandedShadowColor), getCurrentColor(collapsedShadowColor), fraction));

//        ViewCompat.postInvalidateOnAnimation(view);
        view.invalidate();
    }

    private int getCurrentExpandedTextColor() {
        return getCurrentColor(expandedTextColor);
    }

    public int getCurrentCollapsedTextColor() {
        return getCurrentColor(collapsedTextColor);
    }

    
    private int getCurrentColor(int colorStateList) {
//        if (colorStateList == null) {
//            return 0;
//        }
//        if (state != null) {
//            return colorStateList.getColorForState(state, 0);
//        }
//        return colorStateList.getDefaultColor();
        return colorStateList;
    }

    private void calculateBaseOffsets() {
        final float currentTextSize = this.currentTextSize;

        // We then calculate the collapsed text size, using the same logic
        calculateUsingTextSize(collapsedTextSize);
//        if (textToDraw != null && textLayout != null) {
//            textToDrawCollapsed =
//                    TextUtils.ellipsize(textToDraw, textPaint, textLayout.getWidth(), TruncateAt.END);
//        }
        float width =
                textToDrawCollapsed != null
                        ? textPaint.measureText(textToDrawCollapsed)
                        : 0;
        final int collapsedAbsGravity =
                Gravity.getAbsoluteGravity(
                        collapsedTextGravity,
                        isRtl ? Component.LayoutDirection.RTL : Component.LayoutDirection.LTR);

        switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                collapsedDrawY = collapsedBounds.bottom + textPaint.ascent();
                break;
            case Gravity.TOP:
                collapsedDrawY = collapsedBounds.top;
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textOffset = (textPaint.descent() - textPaint.ascent()) / 2;
                collapsedDrawY = collapsedBounds.getCenterY() - textOffset + textPaint.getTextSize();
                break;
        }

        switch (collapsedAbsGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                collapsedDrawX = collapsedBounds.getCenterX() - (width / 2);
                break;
            case Gravity.RIGHT:
                collapsedDrawX = collapsedBounds.right - width;
                break;
            case Gravity.LEFT:
            default:
                collapsedDrawX = collapsedBounds.left;
                break;
        }

        calculateUsingTextSize(expandedTextSize);
        float expandedTextHeight = 0; // textLayout != null ? textLayout.getHeight() : 0;

        float measuredWidth = textToDraw != null
                ? textPaint.measureText(textToDraw) : 0;
        width = measuredWidth; // textLayout != null && maxLines > 1 && !isRtl
                // ? textLayout.getWidth()
                // : measuredWidth;
        expandedFirstLineDrawX = 0; // textLayout != null ? textLayout.getLineLeft(0) : 0;

        final int expandedAbsGravity =
                Gravity.getAbsoluteGravity(
                        expandedTextGravity,
                        isRtl ? Component.LayoutDirection.RTL : Component.LayoutDirection.LTR);
        switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.BOTTOM:
                expandedDrawY = expandedBounds.bottom - expandedTextHeight + textPaint.descent();
                break;
            case Gravity.TOP:
                expandedDrawY = expandedBounds.top;
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                float textOffset = expandedTextHeight / 2;
                expandedDrawY = expandedBounds.getCenterY() - textOffset;
                break;
        }

        switch (expandedAbsGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
            case Gravity.CENTER_HORIZONTAL:
                expandedDrawX = expandedBounds.getCenterX() - (width / 2);
                break;
            case Gravity.RIGHT:
                expandedDrawX = expandedBounds.right - width;
                break;
            case Gravity.LEFT:
            default:
                expandedDrawX = expandedBounds.left;
                break;
        }

        // The bounds have changed so we need to clear the texture
        clearTexture();
        // Now reset the text size back to the original
        setInterpolatedTextSize(currentTextSize);
    }

    private void interpolateBounds(float fraction) {
        currentBounds.left =
                lerp(expandedBounds.left, collapsedBounds.left, fraction, positionInterpolator);
        currentBounds.top = lerp(expandedDrawY, collapsedDrawY, fraction, positionInterpolator);
        currentBounds.right =
                lerp(expandedBounds.right, collapsedBounds.right, fraction, positionInterpolator);
        currentBounds.bottom =
                lerp(expandedBounds.bottom, collapsedBounds.bottom, fraction, positionInterpolator);
    }

    private void setCollapsedTextBlend(float blend) {
        collapsedTextBlend = blend;
//        ViewCompat.postInvalidateOnAnimation(view);
        view.invalidate();
    }

    private void setExpandedTextBlend(float blend) {
        expandedTextBlend = blend;
//        ViewCompat.postInvalidateOnAnimation(view);
        view.invalidate();
    }

    public void draw(Canvas canvas) {
        final int saveCount = canvas.save();
        // Compute where to draw textLayout for this frame
        if (textToDraw != null && drawTitle) {
            final float currentExpandedX =
                    currentDrawX - expandedFirstLineDrawX * 2; //  + textLayout.getLineLeft(0)

            textPaint.setTextSize((int) currentTextSize);
            float x = currentDrawX;
            float y = currentDrawY;
            final boolean drawTexture = useTexture && expandedTitleTexture != null;

            if (DEBUG_DRAW) {
                // Just a debug tool, which drawn a magenta rect in the text bounds
                canvas.drawRect(
                        currentBounds.left,
                        y,
                        currentBounds.right,
                        y + expandedTextSize * scale,// + textLayout.getHeight() * scale,
                        DEBUG_DRAW_PAINT);
            }

            if (scale != 1f) {
                canvas.scale(scale, scale, x, y);
            }

            if (drawTexture) {
                // If we should use a texture, draw it instead of text
                canvas.drawPixelMapHolder(new PixelMapHolder(expandedTitleTexture), x, y, texturePaint);
                canvas.restoreToCount(saveCount);
                return;
            }

            if (shouldDrawMultiline()) {
                drawMultinlineTransition(canvas, currentExpandedX, y);
            } else {
                canvas.translate(x, y);
//                textLayout.draw(canvas);
                canvas.drawText(textPaint, textToDraw, 0, 0);
            }

            canvas.restoreToCount(saveCount);
        }
    }

    private boolean shouldDrawMultiline() {
        return maxLines > 1 && !isRtl && !useTexture;
    }

    private void drawMultinlineTransition(Canvas canvas, float currentExpandedX, float y) {
        float originalAlpha = textPaint.getAlpha();
        // positon expanded text appropriately
        canvas.translate(currentExpandedX, y);
        // Expanded text
        textPaint.setAlpha(expandedTextBlend * originalAlpha);
//        textLayout.draw(canvas);

        // Collapsed text
        textPaint.setAlpha((int) (collapsedTextBlend * originalAlpha));
//        int lineBaseline = textLayout.getLineBaseline(0);
        canvas.drawText(
                textPaint,
                textToDrawCollapsed,
//                /* start = */ 0,
//                textToDrawCollapsed.length(),
                /* x = */ 0,
                0); // lineBaseline
        // Remove ellipsis for Cross-section animation
        String tmp = textToDrawCollapsed.trim();
        if (tmp.endsWith(ELLIPSIS_NORMAL)) {
            tmp = tmp.substring(0, tmp.length() - 1);
        }
        // Cross-section between both texts (should stay at original alpha)
        textPaint.setAlpha(originalAlpha);
        canvas.drawText(
                textPaint,
                tmp,
//                /* start = */ 0,
//                min(textLayout.getLineEnd(0), tmp.length()),
                /* x = */ 0,
                0); // lineBaseline
    }

    private boolean calculateIsRtl(String text) {
        final boolean defaultIsRtl = isDefaultIsRtl();
//        return (defaultIsRtl
//                ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL
//                : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR)
//                .isRtl(text, 0, text.length());
        return defaultIsRtl;
    }

    private boolean isDefaultIsRtl() {
        return view.getLayoutDirection() == Component.LayoutDirection.RTL;
    }

    private void setInterpolatedTextSize(float textSize) {
        calculateUsingTextSize(textSize);

        // Use our texture if the scale isn't 1.0
        useTexture = USE_SCALING_TEXTURE && scale != 1f;

        if (useTexture) {
            // Make sure we have an expanded texture if needed
            ensureExpandedTexture();
        }

//        ViewCompat.postInvalidateOnAnimation(view);
        view.invalidate();
    }

    @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView
    private void calculateUsingTextSize(final float textSize) {
        if (text == null) {
            return;
        }

        float collapsedWidth = collapsedBounds.right - collapsedBounds.left;
        float expandedWidth = expandedBounds.right - expandedBounds.left;

        float availableWidth;
        float newTextSize;
        boolean updateDrawText = false;

        if (isClose(textSize, collapsedTextSize)) {
            newTextSize = collapsedTextSize;
            scale = 1f;
            if (currentTypeface != collapsedTypeface) {
                currentTypeface = collapsedTypeface;
                updateDrawText = true;
            }
            availableWidth = collapsedWidth;
        } else {
            newTextSize = expandedTextSize;
            if (currentTypeface != expandedTypeface) {
                currentTypeface = expandedTypeface;
                updateDrawText = true;
            }
            if (isClose(textSize, expandedTextSize)) {
                // If we're close to the expanded text size, snap to it and use a scale of 1
                scale = 1f;
            } else {
                // Else, we'll scale down from the expanded text size
                scale = textSize / expandedTextSize;
            }

            float textSizeRatio = collapsedTextSize / expandedTextSize;
            // This is the size of the expanded bounds when it is scaled to match the
            // collapsed text size
            float scaledDownWidth = expandedWidth * textSizeRatio;

            // If the scaled down size is larger than the actual collapsed width, we need to
            // cap the available width so that when the expanded text scales down, it matches
            // the collapsed width
            // Otherwise we'll just use the expanded width

            availableWidth = scaledDownWidth > collapsedWidth
                    ? min(collapsedWidth / textSizeRatio, expandedWidth)
                    : expandedWidth;
        }

        if (availableWidth > 0) {
            updateDrawText = (currentTextSize != newTextSize) || boundsChanged || updateDrawText;
            currentTextSize = newTextSize;
            boundsChanged = false;
        }

        if (textToDraw == null || updateDrawText) {
            textPaint.setTextSize((int) currentTextSize);
            textPaint.setFont(currentTypeface);
            // Use linear text scaling if we're scaling the canvas
//            textPaint.setLinearText(scale != 1f);

            isRtl = calculateIsRtl(text);
//            textLayout = createStaticLayout(shouldDrawMultiline() ? maxLines : 1, availableWidth, isRtl);
            textToDraw = text;
        }
    }

//    private StaticLayout createStaticLayout(int maxLines, float availableWidth, boolean isRtl) {
//        StaticLayout textLayout = null;
//        try {
//            textLayout =
//                    StaticLayoutBuilderCompat.obtain(text, textPaint, (int) availableWidth)
//                            .setEllipsize(TruncateAt.END)
//                            .setIsRtl(isRtl)
//                            .setAlignment(ALIGN_NORMAL)
//                            .setIncludePad(false)
//                            .setMaxLines(maxLines)
//                            .build();
//        } catch (StaticLayoutBuilderCompatException e) {
//            Log.e(TAG, e.getCause().getMessage(), e);
//        }
//
//        return checkNotNull(textLayout);
//    }

    private void ensureExpandedTexture() {
//        if (expandedTitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(textToDraw)) {
//            return;
//        }
//
//        calculateOffsets(0f);
//        int width = textLayout.getWidth();
//        int height = textLayout.getHeight();
//
//        if (width <= 0 || height <= 0) {
//            return;
//        }
//
//        expandedTitleTexture = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
//        Canvas c = new Canvas(expandedTitleTexture);
//        textLayout.draw(c);
//
//        if (texturePaint == null) {
//            // Make sure we have a paint
//            texturePaint = new Paint(); // (Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
//            texturePaint.setAntiAlias(true);
//        }
    }

    public void recalculate() {
        if (view.getHeight() > 0 && view.getWidth() > 0) {
            // If we've already been laid out, calculate everything now otherwise we'll wait
            // until a layout
            calculateBaseOffsets();
            calculateCurrentOffsets();
        }
    }

    /**
     * Set the title to display
     *
     * @param text
     */
    public void setText(String text) {
        if (text == null || !TextUtils.equals(this.text, text)) {
            this.text = text;
            textToDraw = null;
            clearTexture();
            recalculate();
        }
    }

    public String getText() {
        return text;
    }

    private void clearTexture() {
        if (expandedTitleTexture != null) {
//            expandedTitleTexture.recycle();
            expandedTitleTexture = null;
        }
    }

    public void setMaxLines(int maxLines) {
        if (maxLines != this.maxLines) {
            this.maxLines = maxLines;
            clearTexture();
            recalculate();
        }
    }

    public int getMaxLines() {
        return maxLines;
    }

    /**
     * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
     * defined as it's difference being < 0.001.
     */
    private static boolean isClose(float value, float targetValue) {
        return Math.abs(value - targetValue) < 0.001f;
    }

    public int getExpandedTextColor() {
        return expandedTextColor;
    }

    public int getCollapsedTextColor() {
        return collapsedTextColor;
    }

    /**
     * Blend {@code color1} and {@code color2} using the given ratio.
     *
     * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
     *     1.0 will return {@code color2}.
     */
    private static int blendColors(int color1, int color2, float ratio) {
        final float inverseRatio = 1f - ratio;
        RgbColor rgbColor1 = new RgbColor(color1 >> 16 & 0xFF, color1 >> 8 & 0xFF, color1 & 0xFF);
        RgbColor rgbColor2 = new RgbColor(color2 >> 16 & 0xFF, color2 >> 8 & 0xFF, color2 & 0xFF);
        float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
        float r = (rgbColor1.getRed() * inverseRatio) + (rgbColor2.getRed() * ratio);
        float g = (rgbColor1.getGreen() * inverseRatio) + (rgbColor2.getGreen() * ratio);
        float b = (rgbColor1.getBlue() * inverseRatio) + (rgbColor2.getBlue() * ratio);
        return Color.argb((int) a, (int) r, (int) g, (int) b);
    }

    private static float lerp(
            float startValue, float endValue, float fraction, TimeInterpolator interpolator) {
        if (interpolator != null) {
            fraction = interpolator.getInterpolation(fraction);
        }
        return AnimationUtils.lerp(startValue, endValue, fraction);
    }

    private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) {
        return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
    }

    private static int convertToArgb(int color) {
        int r, g, b;
        r = color >> 16 & 0xFF ;
        g = color >> 8 & 0xFF ;
        b = color & 0xFF ;

        return (0xFF << 24) | (r << 16) | (g << 8) | b;
    }
}
